﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.CommandLine;
using System.CommandLine.Parsing;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.DotNet.Cli.CommandFactory;
using Microsoft.DotNet.Cli.CommandFactory.CommandResolution;
using Microsoft.DotNet.Cli.CommandLine;
using Microsoft.DotNet.Cli.Commands.Run;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Extensions;
using Microsoft.DotNet.Cli.ShellShim;
using Microsoft.DotNet.Cli.Telemetry;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.DotNet.Configurer;
using Microsoft.Extensions.EnvironmentAbstractions;
using NuGet.Frameworks;
using CommandResult = System.CommandLine.Parsing.CommandResult;

namespace Microsoft.DotNet.Cli;

public class Program
{
    private static readonly string ToolPathSentinelFileName = $"{Product.Version}.toolpath.sentinel";

    public static ITelemetry TelemetryClient;
    public static int Main(string[] args)
    {
        // Register a handler for SIGTERM to allow graceful shutdown of the application on Unix.
        // See https://github.com/dotnet/docs/issues/46226.
        using var termSignalRegistration = PosixSignalRegistration.Create(PosixSignal.SIGTERM, _ => Environment.Exit(0));

        using AutomaticEncodingRestorer _ = new();

        // Setting output encoding is not available on those platforms
        if (UILanguageOverride.OperatingSystemSupportsUtf8())
        {
            Console.OutputEncoding = Encoding.UTF8;
        }

        DebugHelper.HandleDebugSwitch(ref args);

        // Capture the current timestamp to calculate the host overhead.
        DateTime mainTimeStamp = DateTime.Now;
        TimeSpan startupTime = mainTimeStamp - Process.GetCurrentProcess().StartTime;

        bool perfLogEnabled = Env.GetEnvironmentVariableAsBool("DOTNET_CLI_PERF_LOG", false);

        if (string.IsNullOrEmpty(Env.GetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD")))
        {
            Environment.SetEnvironmentVariable("MSBUILDFAILONDRIVEENUMERATINGWILDCARD", "1");
        }

        // Avoid create temp directory with root permission and later prevent access in non sudo
        if (SudoEnvironmentDirectoryOverride.IsRunningUnderSudo())
        {
            perfLogEnabled = false;
        }

        PerformanceLogStartupInformation startupInfo = null;
        if (perfLogEnabled)
        {
            startupInfo = new PerformanceLogStartupInformation(mainTimeStamp);
            PerformanceLogManager.InitializeAndStartCleanup(FileSystemWrapper.Default);
        }

        PerformanceLogEventListener perLogEventListener = null;
        try
        {
            if (perfLogEnabled)
            {
                perLogEventListener = PerformanceLogEventListener.Create(FileSystemWrapper.Default, PerformanceLogManager.Instance.CurrentLogDirectory);
            }

            PerformanceLogEventSource.Log.LogStartUpInformation(startupInfo);
            PerformanceLogEventSource.Log.CLIStart();

            InitializeProcess();

            try
            {
                return ProcessArgs(args, startupTime);
            }
            catch (Exception e) when (e.ShouldBeDisplayedAsError())
            {
                Reporter.Error.WriteLine(CommandLoggingContext.IsVerbose
                    ? e.ToString().Red().Bold()
                    : e.Message.Red().Bold());

                var commandParsingException = e as CommandParsingException;
                if (commandParsingException != null && commandParsingException.ParseResult != null)
                {
                    commandParsingException.ParseResult.ShowHelp();
                }

                return 1;
            }
            catch (Exception e) when (!e.ShouldBeDisplayedAsError())
            {
                // If telemetry object has not been initialized yet. It cannot be collected
                TelemetryEventEntry.SendFiltered(e);
                Reporter.Error.WriteLine(e.ToString().Red().Bold());

                return 1;
            }
            finally
            {
                PerformanceLogEventSource.Log.CLIStop();
            }
        }
        finally
        {
            if (perLogEventListener != null)
            {
                perLogEventListener.Dispose();
            }
        }
    }

    internal static int ProcessArgs(string[] args)
    {
        return ProcessArgs(args, new TimeSpan(0));
    }

    internal static int ProcessArgs(string[] args, TimeSpan startupTime)
    {
        Dictionary<string, double> performanceData = [];

        PerformanceLogEventSource.Log.BuiltInCommandParserStart();
        ParseResult parseResult;
        using (new PerformanceMeasurement(performanceData, "Parse Time"))
        {
            parseResult = Parser.Parse(args);

            // Avoid create temp directory with root permission and later prevent access in non sudo
            // This method need to be run very early before temp folder get created
            // https://github.com/dotnet/sdk/issues/20195
            SudoEnvironmentDirectoryOverride.OverrideEnvironmentVariableToTmp(parseResult);
        }
        PerformanceLogEventSource.Log.BuiltInCommandParserStop();

        using (IFirstTimeUseNoticeSentinel disposableFirstTimeUseNoticeSentinel = new FirstTimeUseNoticeSentinel())
        {
            IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel = disposableFirstTimeUseNoticeSentinel;
            IAspNetCertificateSentinel aspNetCertificateSentinel = new AspNetCertificateSentinel();
            IFileSentinel toolPathSentinel = new FileSentinel(new FilePath(Path.Combine(CliFolderPathCalculator.DotnetUserProfileFolderPath, ToolPathSentinelFileName)));

            PerformanceLogEventSource.Log.TelemetryRegistrationStart();

            TelemetryClient ??= new Telemetry.Telemetry(firstTimeUseNoticeSentinel);
            TelemetryEventEntry.Subscribe(TelemetryClient.TrackEvent);
            TelemetryEventEntry.TelemetryFilter = new TelemetryFilter(Sha256Hasher.HashWithNormalizedCasing);

            PerformanceLogEventSource.Log.TelemetryRegistrationStop();

            if (parseResult.GetValue(Parser.DiagOption) && parseResult.IsDotnetBuiltInCommand())
            {
                // We found --diagnostic or -d, but we still need to determine whether the option should
                // be attached to the dotnet command or the subcommand.
                if (args.DiagOptionPrecedesSubcommand(parseResult.RootSubCommandResult()))
                {
                    Environment.SetEnvironmentVariable(CommandLoggingContext.Variables.Verbose, bool.TrueString);
                    CommandLoggingContext.SetVerbose(true);
                    Reporter.Reset();
                }
            }
            if (parseResult.HasOption(Parser.VersionOption) && parseResult.IsTopLevelDotnetCommand())
            {
                CommandLineInfo.PrintVersion();
                return 0;
            }
            else if (parseResult.HasOption(Parser.InfoOption) && parseResult.IsTopLevelDotnetCommand())
            {
                CommandLineInfo.PrintInfo();
                return 0;
            }
            else
            {
                PerformanceLogEventSource.Log.FirstTimeConfigurationStart();

                var environmentProvider = new EnvironmentProvider();

                bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true);
                bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault);
                bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true);
                bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false);
                bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK,
                    // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false.
                    defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment());

                ReportDotnetHomeUsage(environmentProvider);

                var isDotnetBeingInvokedFromNativeInstaller = false;
                if (parseResult.CommandResult.Command.Name.Equals(Parser.InstallSuccessCommand.Name))
                {
                    aspNetCertificateSentinel = new NoOpAspNetCertificateSentinel();
                    firstTimeUseNoticeSentinel = new NoOpFirstTimeUseNoticeSentinel();
                    toolPathSentinel = new NoOpFileSentinel(exists: false);
                    isDotnetBeingInvokedFromNativeInstaller = true;
                }

                var dotnetFirstRunConfiguration = new DotnetFirstRunConfiguration(
                    generateAspNetCertificate: generateAspNetCertificate,
                    telemetryOptout: telemetryOptout,
                    addGlobalToolsToPath: addGlobalToolsToPath,
                    nologo: nologo,
                    skipWorkloadIntegrityCheck: skipWorkloadIntegrityCheck);

                string[] getStarOperators = ["getProperty", "getItem", "getTargetResult"];
                char[] switchIndicators = ['-', '/'];
                var getStarOptionPassed = parseResult.CommandResult.Tokens.Any(t =>
                    getStarOperators.Any(o =>
                    switchIndicators.Any(i => t.Value.StartsWith(i + o, StringComparison.OrdinalIgnoreCase))));

                ConfigureDotNetForFirstTimeUse(
                    firstTimeUseNoticeSentinel,
                    aspNetCertificateSentinel,
                    toolPathSentinel,
                    isDotnetBeingInvokedFromNativeInstaller,
                    dotnetFirstRunConfiguration,
                    environmentProvider,
                    performanceData,
                    skipFirstTimeUseCheck: getStarOptionPassed);
                PerformanceLogEventSource.Log.FirstTimeConfigurationStop();
            }
        }

        if (CommandLoggingContext.IsVerbose)
        {
            Console.WriteLine($"Telemetry is: {(TelemetryClient.Enabled ? "Enabled" : "Disabled")}");
        }
        PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStart();
        performanceData.Add("Startup Time", startupTime.TotalMilliseconds);

        string globalJsonState = string.Empty;
        if (TelemetryClient.Enabled)
        {
            // Get the global.json state to report in telemetry along with this command invocation.
            globalJsonState = NativeWrapper.NETCoreSdkResolverNativeWrapper.GetGlobalJsonState(Environment.CurrentDirectory);
        }

        TelemetryEventEntry.SendFiltered(Tuple.Create(parseResult, performanceData, globalJsonState));
        PerformanceLogEventSource.Log.TelemetrySaveIfEnabledStop();

        int exitCode;
        if (parseResult.CanBeInvoked())
        {
            InvokeBuiltInCommand(parseResult, out exitCode);
        }
        else
        {
            PerformanceLogEventSource.Log.ExtensibleCommandResolverStart();
            try
            {
                string commandName = "dotnet-" + parseResult.GetValue(Parser.DotnetSubCommand);
                var resolvedCommandSpec = CommandResolver.TryResolveCommandSpec(
                    new DefaultCommandResolverPolicy(),
                    commandName,
                    args.GetSubArguments(),
                    FrameworkConstants.CommonFrameworks.NetStandardApp15);

                if (resolvedCommandSpec is null && TryRunFileBasedApp(parseResult) is { } fileBasedAppExitCode)
                {
                    exitCode = fileBasedAppExitCode;
                }
                else
                {
                    var resolvedCommand = CommandFactoryUsingResolver.CreateOrThrow(commandName, resolvedCommandSpec);
                    PerformanceLogEventSource.Log.ExtensibleCommandResolverStop();

                    PerformanceLogEventSource.Log.ExtensibleCommandStart();
                    var result = resolvedCommand.Execute();
                    PerformanceLogEventSource.Log.ExtensibleCommandStop();

                    exitCode = result.ExitCode;
                }
            }
            catch (CommandUnknownException e)
            {
                Reporter.Error.WriteLine(e.Message.Red());
                Reporter.Output.WriteLine(e.InstructionMessage);
                exitCode = 1;
            }
        }

        TelemetryClient.TrackEvent("command/finish", properties: new Dictionary<string, string>
                    {
                        { "exitCode", exitCode.ToString() }
                    },
            measurements: new Dictionary<string, double>());

        PerformanceLogEventSource.Log.TelemetryClientFlushStart();
        TelemetryClient.Flush();
        PerformanceLogEventSource.Log.TelemetryClientFlushStop();

        TelemetryClient.Dispose();

        return exitCode;

        static int? TryRunFileBasedApp(ParseResult parseResult)
        {
            // If we didn't match any built-in commands, and a C# file path is the first argument,
            // parse as `dotnet run --file file.cs ..rest_of_args` instead.
            if (parseResult.GetValue(Parser.DotnetSubCommand) is { } unmatchedCommandOrFile
                && VirtualProjectBuildingCommand.IsValidEntryPointPath(unmatchedCommandOrFile))
            {
                List<string> otherTokens = new(parseResult.Tokens.Count - 1);
                foreach (var token in parseResult.Tokens)
                {
                    if (token.Type != TokenType.Argument || token.Value != unmatchedCommandOrFile)
                    {
                        otherTokens.Add(token.Value);
                    }
                }

                parseResult = Parser.Parse(["run", "--file", unmatchedCommandOrFile, .. otherTokens]);

                InvokeBuiltInCommand(parseResult, out var exitCode);
                return exitCode;
            }

            return null;
        }

        static void InvokeBuiltInCommand(ParseResult parseResult, out int exitCode)
        {
            Debug.Assert(parseResult.CanBeInvoked());

            PerformanceLogEventSource.Log.BuiltInCommandStart();

            try
            {
                exitCode = Parser.Invoke(parseResult);
                exitCode = AdjustExitCode(parseResult, exitCode);
            }
            catch (Exception exception)
            {
                exitCode = Parser.ExceptionHandler(exception, parseResult);
            }

            PerformanceLogEventSource.Log.BuiltInCommandStop();
        }
    }

    private static int AdjustExitCode(ParseResult parseResult, int exitCode)
    {
        if (parseResult.Errors.Count > 0)
        {
            var commandResult = parseResult.CommandResult;

            while (commandResult is not null)
            {
                if (commandResult.Command.Name == "new")
                {
                    // default parse error exit code is 1
                    // for the "new" command and its subcommands it needs to be 127
                    return 127;
                }

                commandResult = commandResult.Parent as CommandResult;
            }
        }

        return exitCode;
    }

    private static void ReportDotnetHomeUsage(IEnvironmentProvider provider)
    {
        var home = provider.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName);
        if (string.IsNullOrEmpty(home))
        {
            return;
        }

        Reporter.Verbose.WriteLine(
            string.Format(
                LocalizableStrings.DotnetCliHomeUsed,
                home,
                CliFolderPathCalculator.DotnetHomeVariableName));
    }

    private static void ConfigureDotNetForFirstTimeUse(
       IFirstTimeUseNoticeSentinel firstTimeUseNoticeSentinel,
       IAspNetCertificateSentinel aspNetCertificateSentinel,
       IFileSentinel toolPathSentinel,
       bool isDotnetBeingInvokedFromNativeInstaller,
       DotnetFirstRunConfiguration dotnetFirstRunConfiguration,
       IEnvironmentProvider environmentProvider,
       Dictionary<string, double> performanceMeasurements,
       bool skipFirstTimeUseCheck)
    {
        var isFirstTimeUse = !firstTimeUseNoticeSentinel.Exists() && !skipFirstTimeUseCheck;
        var environmentPath = EnvironmentPathFactory.CreateEnvironmentPath(isDotnetBeingInvokedFromNativeInstaller, environmentProvider);
        _ = new DotNetCommandFactory(alwaysRunOutOfProc: true);
        var aspnetCertificateGenerator = new AspNetCoreCertificateGenerator();
        var reporter = Reporter.Error;
        var dotnetConfigurer = new DotnetFirstTimeUseConfigurer(
            firstTimeUseNoticeSentinel,
            aspNetCertificateSentinel,
            aspnetCertificateGenerator,
            toolPathSentinel,
            dotnetFirstRunConfiguration,
            reporter,
            environmentPath,
            performanceMeasurements,
            skipFirstTimeUseCheck: skipFirstTimeUseCheck);

        dotnetConfigurer.Configure();

        if (isDotnetBeingInvokedFromNativeInstaller && OperatingSystem.IsWindows())
        {
            DotDefaultPathCorrector.Correct();
        }

        if (isFirstTimeUse && !dotnetFirstRunConfiguration.SkipWorkloadIntegrityCheck)
        {
            try
            {
                WorkloadIntegrityChecker.RunFirstUseCheck(reporter);
            }
            catch (Exception)
            {
                // If the workload check fails for any reason, we want to eat the failure and continue running the command.
                reporter.WriteLine(CliStrings.WorkloadIntegrityCheckError.Yellow());
            }
        }
    }

    private static void InitializeProcess()
    {
        // by default, .NET Core doesn't have all code pages needed for Console apps.
        // see the .NET Core Notes in https://docs.microsoft.com/dotnet/api/system.diagnostics.process#-notes
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        UILanguageOverride.Setup();
    }
}
